﻿/*
 *  This file is part of SXAPI.
 *
 *  SXAPI is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, version 3 of the License.

 *  SXAPI is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with SXAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using Jayrock.Json;
using System.IO;
using System.Xml;
using System.Globalization;

namespace StackExchangeApi
{
    /// <summary>
    /// Represents an Answer on one of the "Trilogy Sites"
    /// </summary>
    public sealed class Answer : Post
    {
        private Object m_set_post_sync = new Object();

        [BacksCache]
        private volatile bool m_published_set = false;
        private DateTime m_published;

        [BacksCache]
        private volatile bool m_updated_set = false;
        private DateTime m_updated;

        [BacksCache]
        private volatile bool m_accepted_set = false;
        private bool m_accepted;

        /// <summary>
        /// Converts an EagerAnswer into an Answer.
        /// 
        /// Although the Answer would normally be lazily-loaded,
        /// since it is initialized with a fully populated EagerAnswer
        /// no loading at all will occur when its properties are
        /// accessed.
        /// </summary>
        /// <param name="answer">EagerAnswer to cast</param>
        /// <returns>An Answer as above.</returns>
        public static implicit operator Answer(EagerAnswer answer)
        {
            return AnswerCache.GetOrAllocate(answer.Id, answer.Site);
        }

        /// <summary>
        /// Whether this answer was accepted or not
        /// </summary>
        public bool AcceptedAnswer
        {
            get
            {
                if (!m_accepted_set)
                {
                    lock (m_set_post_sync)
                    {
                        if(!m_accepted_set)
                            SetPostValues(this);
                    }
                }

                return m_accepted;
            }

            private set
            {
                m_accepted = value;
                m_accepted_set = true;
            }
        }

        /// <summary>
        /// The date and time this answer was created
        /// </summary>
        public DateTime Published
        {
            get
            {
                if (!m_published_set)
                {
                    lock (m_set_post_sync)
                    {
                        if (!m_published_set)
                            SetPostValues(this);
                    }
                }

                return m_published;
            }

            internal set { m_published = value; m_published_set = true; }
        }

        /// <summary>
        /// The last time this answer was updated
        /// </summary>
        public DateTime Updated
        {
            get
            {
                if (!m_updated_set)
                {
                    lock (m_set_post_sync)
                    {
                        if (!m_updated_set)
                            SetPostValues(this);
                    }
                }

                return m_updated;
            }

            internal set { m_updated = value; m_updated_set = true; }
        }

        /// <summary>
        /// Construct a new Answer
        /// 
        /// Note that this will succeed even if the Answer does not exist
        /// </summary>
        /// <param name="pid">Id of the answer</param>
        /// <param name="site">Site the answer is on</param>
        internal Answer(long pid, StackExchangeSite site) : base(pid, site) { }

        /// <summary>
        /// Create a new Answer.
        /// Note that this will succeed even if the ID is invalid,
        /// but subsequent requests on properties will fail.
        /// </summary>
        /// <param name="id">the id of the answer</param>
        /// <param name="site">the site the answer is on</param>
        /// <returns>An Answer that can be used to fetch values associated with the id/site combo</returns>
        public static Answer GetAnswerWithId(long id, StackExchangeSite site)
        {
            return AnswerCache.GetOrAllocate(id, site);
        }

        /// <summary>
        /// Sets the Answers property on the given User
        /// </summary>
        /// <param name="user">User in question</param>
        internal static void SetAnswersFor(User user)
        {
            DelayedChunkProvider<Answer> answersProvider =
                delegate(int chunk, out int chunkCount)
                {
                    List<Answer> answers = new List<Answer>();

                    String str = ConnectionMediator.MakeRequest(GetAnswerListUrl(user, chunk + 1));
                    JsonReader reader = new JsonTextReader(new StringReader(str));

                    JsonObject obj = new JsonObject();
                    obj.Import(reader);

                    JsonArray posts = (JsonArray)obj["Posts"];

                    for (int i = 0; i < posts.Length; i++)
                    {
                        JsonObject post = (JsonObject)posts[i];

                        Answer ans = Answer.GetAnswerWithId(((JsonNumber)post["Id"]).ToInt64(), user.Site);
                        ans.AcceptedAnswer = (bool)(post["AcceptedAnswer"]);
                        ans.Title = (String)post["Title"];
                        ans.Votes = ((JsonNumber)post["VoteCount"]).ToInt64();

                        answers.Add(ans);
                    }

                    int totalPosts = ((JsonNumber)obj["Total"]).ToInt32();

                    chunkCount = totalPosts / 50;
                    if (totalPosts % 50 != 0) chunkCount++;

                    return answers;
                };

            user.Answers = new ReadOnlyDelayLoadedList<Answer>(new DelayLoadedList<Answer>(answersProvider, 50));
        }

        /// <summary>
        /// Construct a Uri that gets one page (of size 50) of answers that the given
        /// user has posted.
        /// </summary>
        /// <param name="user">The user in question</param>
        /// <param name="pageNum">The page number to fetch</param>
        /// <returns>A Uri that will return a JSON encoded list of answers when fetched</returns>
        private static Uri GetAnswerListUrl(User user, int pageNum)
        {
            String str = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/api/useranswers.json?userid={1}&page={2}&pagesize={3}&sort={4}",
                Utils.GetAddressForSite(user.Site),
                user.Id,
                pageNum,
                50,
                "recent");

            return new Uri(str);
        }

        /// <summary>
        /// Parse the values of an answer out of the RSS feed, given the entry that serves as its parent.
        /// 
        /// Fills in the special key "TYPE" with its best guess as to what this entry represents,
        /// either an "answer", a "question", or a "comment".
        /// </summary>
        /// <param name="entry">XmlNode that represents the root node of the question</param>
        /// <param name="vals">Dictionary to populate with values</param>
        internal static void ParseValues(XmlNode entry, Dictionary<String, String> vals)
        {
            foreach (XmlNode node in entry)
            {
                XmlNode copy = node;

                String val = "";

                while (copy.HasChildNodes) copy = copy.FirstChild;

                val = copy.InnerText;

                vals[node.Name] = val;
            }


            //Perform some fixup of these values for easier parsing
            vals["published"] = vals["published"].Replace("T", " ");
            vals["updated"] = vals["updated"].Replace("T", " ");

            String ansLeadin = String.Format(CultureInfo.InvariantCulture, "Answer by {0} for ", vals["author"]);
            String comLeadin = String.Format(CultureInfo.InvariantCulture, "Comment by {0} on ", vals["author"]);

            bool isAnswer = false;
            bool isComment = false;

            if (vals["title"].StartsWith(ansLeadin, StringComparison.Ordinal))
            {
                isAnswer = true;
                vals["title"] = vals["title"].Substring(ansLeadin.Length);
            }

            if (vals["title"].StartsWith(comLeadin, StringComparison.Ordinal))
            {
                isComment = true;
                vals["title"] = vals["title"].Substring(comLeadin.Length);
            }

            if (isAnswer)
                vals["TYPE"] = "answer";
            else
                if (isComment) 
                    vals["TYPE"] = "comment";
                else
                    vals["TYPE"] = "question";
        }

        /// <summary>
        /// Populate an Answer's properties
        /// </summary>
        /// <param name="other">Answer to populate</param>
        internal override void SetPostValues(Post other)
        {
            Answer ans = (Answer)other;

            Dictionary<String, String> vals = new Dictionary<string, string>();

            String xml = ConnectionMediator.MakeRequest(GetAnswerValuesUri(ans.Id, ans.Site));

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            XmlNode entry = doc.LastChild.LastChild;

            ParseValues(entry, vals);

            ans.Author = vals["author"];
            ans.Published = DateTime.ParseExact(vals["published"], "u", DateTimeFormatInfo.InvariantInfo);
            ans.Updated = DateTime.ParseExact(vals["updated"], "u", DateTimeFormatInfo.InvariantInfo);
            ans.Summary = vals["summary"];

            //Defined in Post
            ans.Title = vals["title"];
            ans.Votes = Int64.Parse(vals["re:rank"], CultureInfo.InvariantCulture);

            //TODO:  Figure out a way to ACTUALLY get this instead of defaulting to false
            ans.AcceptedAnswer = false;
        }

        /// <summary>
        /// Constructs a Uri that, when fetched, will get the RSS feed that contains
        /// a single answer.
        /// </summary>
        /// <param name="id">Id of the Answer to fetch</param>
        /// <param name="site">Site the Answer exists on</param>
        /// <returns>A Uri as described above</returns>
        private static Uri GetAnswerValuesUri(long id, StackExchangeSite site)
        {
            String str = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/feeds/question/{1}", 
                Utils.GetAddressForSite(site), 
                id);

            return new Uri(str);
        }

        /// <summary>
        /// Custom Equality operation.
        /// 
        /// Note that it IS possible for an EagerAnswer to be
        /// equivalent to an Answer.
        /// </summary>
        /// <param name="obj">To test for equality</param>
        /// <returns>true if equal, false otherwise</returns>
        public override bool Equals(object obj)
        {
            Answer otherAns = obj as Answer;
            EagerAnswer otherEAns = obj as EagerAnswer;

            if (otherAns != null)
            {
                return otherAns.Site.Equals(Site) &&
                       otherAns.Id.Equals(Id);
            }

            if (otherEAns != null)
            {
                return otherEAns.Site.Equals(Site) &&
                       otherEAns.Id.Equals(Id);
            }

            return false;
        }

        /// <summary>
        /// Custom hash-code implementation
        /// </summary>
        /// <returns>A hashcode for this Answer</returns>
        public override int GetHashCode()
        {
            return Id.GetHashCode() ^
                   (-1 * Site.GetHashCode());
        }
    }

    /// <summary>
    /// Class for managing the internal cache of all
    /// allocated Answers.
    /// 
    /// Used so that allocating new Answers doesn't result in
    /// duplicate requests to populate it.
    /// </summary>
    internal sealed class AnswerCache : AllocationCache<Answer> { }
}